Redis 是一個基於記憶體的 key value 資料庫,在過去最常用來當作 快取(Cache) 的媒介,隨著 Redis 日漸茁壯,開始提供持久性的功能,使它也能夠用來當作 文件資料庫(Document Dadabase),甚至還提供了 消息代理(Message Broker) 的功能,可說是非常強大的工具。
在終端機輸入下方指令以便從 Docker Hub 下載 Redis 的 Docker Image:
$ docker pull redis
下載後,透過下方指令將 Redis 架設在 6379
port:
$ docker run --name <NAME> -p 6379:6379 -d redis
透過 Container 名稱即可對該 Container 下指令,進而使用 Redis CLI 來操作 Redis。透過下方指令執行 Container 內的 bash:
$ docker exec -it <NAME> bash
接著,輸入下方指令進入 Redis CLI:
$ redis-cli
如此一來,便可以對 Redis 進行操作。透過下方指令進行測試,會在終端機顯示 Pong
:
$ Ping
前面提到 Redis 不僅可以用來當作 Cache 的媒介,還可以用來實作 Message Broker,那該如何實作呢?Redis 在第二版的時候新增 發佈訂閱(Publish/Subscribe) 的機制,簡稱 Redis Pub/Sub,讓服務之間可以透過 Redis 來交換訊息,只要 訂閱方(Subscriber) 訂閱 發佈方(Publisher) 所指定的 通道(Channel),當 Publisher 發送了一則訊息到該通道時,Subscriber 就會收到該訊息,以架構面來說,Subscriber 並不會直接與 Publisher 進行溝通,而是透過 Redis 作為 Message Broker,如此一來便解除了Subscriber 與 Publisher 之間的耦合關係。
在讓兩個服務透過該機制進行交換訊息之前,可以先試著用 Redis CLI 來操作,首先,打開兩個終端機並進入 Redis CLI,在其中一個終端機輸入下方指令來訂閱名稱為 test-channel
的通道:
$ SUBSCRIBE test-channel
接著,透過另一個終端機輸入下方指令發送訊息到名稱為 test-channel
的通道:
$ PUBLISH test-channel "Hello World"
此時 Subscriber 會收到訊息:
如果要取消訂閱可以使用下方指令:
$ UNSUBSCRIBE test-channel
Redis Pub/Sub 支援透過 Pattern 的方式來接收訊息,透過下方指令訂閱通道名稱符合 order.*
的訊息:
$ PSUBSCRIBE order.*
此時對 order.created
發送訊息:
$ PUBLISH order.created "Hello World"
Subscriber 會順利收到發送的訊息:
注意:Redis Pub/Sub 並不會保存已發出的訊息,所以 如果 Subscriber 是在訊息 發佈後 才進行訂閱,那就會收不到該筆訊息,進而造成資料丟失的情況。
NestJS 實作了 Redis Transporter,讓微服務應用程式可以用跟其他 Transporter 一樣的開發風格來使用 Redis Pub/Sub,當服務之間的溝通錯綜複雜時,相較於服務間直接溝通會是更好的選擇。
要使用 Redis Transporter 之前,需要先安裝下方套件:
$ npm install ioredis
補充:
ioredis
是一套用於 Node.js 的強大、功能齊全 的 Redis 客戶端,有興趣可以參考官方文件。
修改載入點 main.ts
的內容,將 transport
設定為 Transport.REDIS
,並根據架設的 Redis 位址來設定 host
與 port
的資訊:
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.REDIS,
options: {
host: '0.0.0.0',
port: 6379,
},
},
);
await app.listen();
}
bootstrap();
上方範例在 options
只使用了 host
與 port
,事實上,Redis Transporter 有下列五個屬性可以設定:
host
:要連線的主機,如:localhost
。port
:指定要連線的主機 port,如:3333
。retryAttempts
:當無法連至主機時的重試次數,預設是 0
。retryDelay
:每次重試的間隔時間,以毫秒(ms)為單位,預設是 0
。wildcards
:是否啟用 Redis Pub/Sub 的 Pattern 功能,預設是 false
。Redis Transporter 也支援 Request-response 與 Event-based 訊息模式。下方是範例程式碼,在 AppController
內實作 sayHello
方法並套用 @MessagePattern
裝飾器,以及實作 onOrderCreated
方法並套用 @EventPattern
裝飾器:
import { Controller } from '@nestjs/common';
import { EventPattern, MessagePattern } from '@nestjs/microservices';
@Controller()
export class AppController {
@MessagePattern({ cmd: 'hello' })
sayHello(data: string) {
console.log(data);
return `Hello, ${data}`;
}
@EventPattern('order.created')
onOrderCreated(order: { name: string }) {
console.log(order);
}
}
使用 Redis CLI 進行測試,透過下方指令將訊息發送到 order.created
通道:
$ PUBLISH order.created "Hello World"
此時在微服務應用程式的終端機會顯示發送的訊息:
那如果 Pattern 設定成可序列化物件的話,要如何透過 Redis CLI 發送訊息呢?又或是使用其他技術的微服務應用程式該如何發送訊息呢?很簡單,將該可序列化物件轉成 JSON 字串即可,以上方程式碼來說,我們要將 { cmd: 'hello' }
轉成字串 '{"cmd":"hello"}'
當作通道名稱:
$ PUBLISH '{"cmd":"hello"}' "Hello World"
此時在微服務應用程式的終端機會顯示發送的訊息:
假如要取得該請求的相關資訊,比如:通道名稱,可以透過 @Ctx
裝飾器取得 RedisContext
。下方是範例程式碼:
import { Controller } from '@nestjs/common';
import {
Ctx,
EventPattern,
MessagePattern,
Payload,
RedisContext
} from '@nestjs/microservices';
@Controller()
export class AppController {
@EventPattern('order.created')
onOrderCreated(
@Payload() order: { name: string },
@Ctx() ctx: RedisContext
) {
console.log(ctx.getChannel());
console.log(order);
}
}
使用 Redis CLI 進行測試,透過下方指令將訊息發送到 order.created
通道:
$ PUBLISH order.created "Hello World"
此時在微服務應用程式的終端機會顯示發送的訊息以及通道名稱:
修改 AppModule
的內容,透過 ClientsModule
建立 Redis Transporter 的 ClientProxy
:
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
// ...
@Module({
// ...
imports: [
ClientsModule.register([
{
name: 'REDIS_SERVICE',
transport: Transport.REDIS,
options: {
host: '0.0.0.0',
port: 6379
}
}
])
]
})
export class AppModule {}
由於 Redis Transporter 支援 Request-response 與 Event-based 訊息模式,所以可以使用 ClientProxy
的 send
與 emit
方法。下方是範例程式碼,修改 AppController
的內容,使用 @Inject
裝飾器注入 ClientProxy
,並設計 getHello
與 onOrderCreated
方法:
import { Inject, Controller, Get } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
@Controller()
export class AppController {
constructor(
@Inject('REDIS_SERVICE') private readonly redisService: ClientProxy
) {}
@Get()
getHello() {
return this.redisService.send({ cmd: 'hello' }, 'HAO');
}
@Get('orderCreated')
onOrderCreated() {
return this.redisService.emit('order.created', { name: 'test' });
}
}
透過 Postman 使用 GET
方法存取 http://localhost:3000,會看到下方的結果:
透過 Postman 使用 GET
方法存取 http://localhost:3000/orderCreated,在微服務應用程式的終端機會看到 { name: 'test' }
:
Redis Pub/Sub 是單向傳輸訊息的運作模式,那麼 NestJS 是如何實現 Request-response 訊息模式的呢?NestJS 會讓微服務應用程式訂閱指定通道名稱的訊息,比如:order.created
,在回覆訊息時,會向 order.created.reply
通道發送訊息,NestJS 客戶端會訂閱該通道以獲取回應,這也是為什麼前面提到 Request-response 會消耗較多資源的主因。
我們可以透過 Redis CLI 來觀察這個行為,透過下方指令訂閱所有通道的訊息:
$ PSUBSCRIBE *
接著,透過 Postman 使用 GET
方法存取 http://localhost:3000,會在 Redis CLI 上看見訂閱的通道訊息以及對應 reply 通道的訊息:
Redis 是一個強大的工具,經常用來當作 Cache 的媒介,甚至在後面的版本可以用來當作 Document Database 以及 Message Broker,也因為 Redis 提供了 Redis Pub/Sub 功能,讓服務之間可以透過這個功能實現 Publish/Subscribe Pattern,進而消除服務之間的耦合。
NestJS 設計了 Redis Transporter 讓開發者可以用跟其他 Transporter 一樣的開發風格來使用 Redis Pub/Sub。
由於 Redis Pub/Sub 本身是單向傳輸訊息的運作模式,NestJS 在實現 Request-response 訊息模式時,必須額外開啟 reply 通道,這也是為什麼前面提到會消耗較多資源的主因。